package butines.core.game;

import static butines.core.game.AudioUtils.convertFormat;
import static butines.core.game.AudioUtils.isPcm;
import static butines.core.game.AudioUtils.isSameFormat;
import static butines.core.game.AudioUtils.readFully;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFileFormat.Type;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

public class Audio {

	private Type typeAudio;
	private AudioFormat playbackFormat;
	private int numLine;
	
	private LinkedList<Sound> queue;
	private ExecutorService executor;
	private boolean alive;
	
	private Map<String, Sound> cacheSound;
	
	public Audio(AudioFormat playbackFormat, Type typeAudio, int numLine) {
		this.playbackFormat = playbackFormat;
		this.typeAudio = typeAudio;
		this.numLine = numLine;
		this.queue = new LinkedList<Sound>();
		this.cacheSound = new HashMap<String, Sound>();
	}

	public void init() {
		this.alive = true;
		this.executor = Executors.newFixedThreadPool(numLine);
		for (int i = 0; i < numLine; i++) {
			executor.execute(new Worker());
		}
	}
	
	public void destroy() {
		if (alive) {
			synchronized(this) {
				alive = false;
				queue.clear();
				notifyAll();
			}
			executor.shutdown();
		}
	}
	
	public AudioFormat getPlaybackFormat() {
		return playbackFormat;
	}

	
	private Sound createSound(String path) throws UnsupportedAudioFileException, IOException {
		File sourceFile = new File(path);

		AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(sourceFile);
		if (fileFormat.getType() != typeAudio) {
			throw new IllegalArgumentException("Invalid type audio");
		}

		AudioInputStream aiStream = AudioSystem.getAudioInputStream(sourceFile);
		AudioFormat format = aiStream.getFormat();

		try {
			if (isPcm(format.getEncoding()) == false) {
				throw new IllegalArgumentException("Encoding of source audio data is not PCM; conversion not possible");
			}
		
			if (isSameFormat(playbackFormat, format) == false) {
				aiStream = convertFormat(aiStream, playbackFormat, typeAudio);
			}
			
			byte[] samples = readFully(aiStream, playbackFormat);
			return new Sound(this, path, samples);
		} finally {
			aiStream.close();
		}
	}
	
	public void clearCacheSound() {
		cacheSound.clear();
	}

	public Sound load(String path){
		if (cacheSound.containsKey(path)) {
			return cacheSound.get(path);
		}
		try {
			Sound sound = createSound(path);
			cacheSound.put(path, sound);
			return sound;
		} catch (Exception e) {
			throw new RuntimeException();
		}
	}

	public synchronized void play(Sound sound) {
		if (alive == false) {
			throw new IllegalStateException("shutdown is call");
		}
		queue.add(sound);
		notify();
	}
	
	private synchronized Sound getSound() throws InterruptedException {
        while (queue.size() == 0) {
        	if (alive == false) {
                return null;
            }
            wait();
        }
        return (Sound)queue.removeFirst();
    }



	protected class Worker implements Runnable {

    	private int bufferSize;
		private byte[] buffer;
		private SourceDataLine line;
		
		public Worker() {
			// use a short, 100ms (1/10th sec) buffer for real-time change to the sound stream
			int frameSize = playbackFormat.getFrameSize();
			int sampleRate = Math.round(playbackFormat.getSampleRate() / 10);
			bufferSize = frameSize * sampleRate;
			buffer = new byte[bufferSize];
		}

		private void init() throws LineUnavailableException {
			// create a line to play to
			DataLine.Info info = new DataLine.Info(SourceDataLine.class, playbackFormat);
			line = (SourceDataLine) AudioSystem.getLine(info);
			line.open(playbackFormat, bufferSize);
			// start the line
			line.start();
		}
		
		private void destory() {
			// wait until all data is played
			line.drain();
			// close the line
			line.close();
		}
		
		private void play(InputStream iStream){
			try {
				int numBytesRead = 0;
				while (numBytesRead != -1) {
					numBytesRead = iStream.read(buffer, 0, buffer.length);
					if (numBytesRead != -1) {
						line.write(buffer, 0, numBytesRead);
					}
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		
		public void run() {
			try {
				init();
				
				while (alive) {
	                // get a sound to play
	                Sound sound = null;
	                try {
	                    sound = getSound();
	                } catch (InterruptedException ex) {
	                	// ignored
	                }
	                // if getSound() returned null or was interrupted, close this thread.
	                if (sound == null) {
	                    break;
	                }
	                // play the sound, and eat any exceptions it throws
	                try {
	                	play(new ByteArrayInputStream(sound.getSamples()));
	                } catch (Throwable t) {
	                    t.printStackTrace();
	                }
	            }
				
				destory();
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
		
	}

}
